package org.fhm.zdte.client.cmd;

import org.fhm.substrate.annotation.BeanInitial;
import org.fhm.substrate.annotation.Component;
import org.fhm.substrate.annotation.Setup;
import org.fhm.substrate.service.LoggerHandler;
import org.fhm.substrate.standard.ILogger;
import org.fhm.zdte.client.ability.*;
import org.fhm.zdte.client.api.ClientApi;
import org.fhm.zdte.client.service.AbstractClientStarter;
import org.fhm.zdte.common.ability.IGetIdentification;
import org.fhm.zdte.common.ability.IGetServerAddress;
import org.fhm.zdte.common.ability.IGetTargetAddress;
import org.fhm.zdte.common.constant.ChannelType;
import org.fhm.zdte.common.constant.protocol.ConnectStatus;
import org.fhm.zdte.common.constant.protocol.PacketType;
import org.fhm.zdte.common.constant.protocol.ResponseMessageType;
import org.fhm.zdte.common.constant.subcmd.ConnectCmd;
import org.fhm.zdte.common.pojo.Address;
import org.fhm.zdte.common.pojo.Result;
import org.fhm.zdte.common.pool.ThreadPoolFactory;
import org.fhm.zdte.common.service.api.CommonThreadManager;
import org.fhm.zdte.common.standard.IThreadManager;
import org.fhm.zdte.common.util.CommonUtil;

import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * Connect command processor.
 *
 * @author 谭波
 * @since 2023/11/23
 */
@Component("CONNECT")
public class ConnectClientCmd extends AbstractClientCmd implements
        IGetConnectResult, IConnectTimeoutRun, IStartTwoStepsConnect, IAddConnectTask,
        IGetTargetAddress, IConnectInitialize, IGetConnectState, IGetConnectChannelType {
    private static final Map<String, Result<Void>> connectResult = new ConcurrentHashMap<>();
    private static final Map<Address, ChannelType> channelTypeMap = new ConcurrentHashMap<>();
    private static Address localPublicAddress;
    private final ILogger logger = LoggerHandler.getLogger(ConnectClientCmd.class);

    @Setup
    private AbstractClientStarter abstractClientStarter;

    @Setup
    private ThreadPoolFactory threadPoolFactory;

    @Setup
    private IThreadManager iThreadManager;

    @Setup
    private IGetIdentification iGetIdentification;

    @Setup
    private CommonUtil commonUtil;

    @Setup
    private IGetServerAddress iGetServerAddress;

    @Setup
    private ClientApi clientApi;

    @BeanInitial
    private void beanInitial() {
        subPacketDataDealMap.get(PacketType.CONNECT.getName())
                .putConsumer(
                        ConnectCmd.START_CONNECT,
                        (address, message) -> {
                            String to = message.getFrom();
                            if (abstractMessageMonitor.isFailMessage(message)) {
                                connectResult.put(
                                        to,
                                        Result.getFailMessage(
                                                ResponseMessageType.getResponseDescByByte(
                                                        message.getResponseMessageType()
                                                )
                                        )
                                );
                                timeRecordContainer.remove(to);
                                iThreadManager.notify(message.getMessageNumber());
                            } else {
                                Address tempAddress = message.getToAddress();
                                if (Objects.isNull(localPublicAddress) || !localPublicAddress.equals(tempAddress)) {
                                    localPublicAddress = tempAddress;
                                    logger.info("your local public ip is {}, port is {}",
                                            localPublicAddress.getIpv4(), localPublicAddress.getPort());
                                }
                                iThreadManager.notify(message.getMessageNumber());
                                if (Objects.isNull(connectUserContainer.get(to))) {
                                    synchronized (this) {
                                        if (Objects.isNull(connectUserContainer.get(to))) {
                                            Address toAddress = message.getFromAddress();
                                            toAddress.setStatus(ConnectStatus.CONNECTING);
                                            connectUserContainer.put(to, toAddress);
                                            commonUtil.cancelTask(connectTaskContainer.remove(to));
                                            connectTaskContainer.put(
                                                    to,
                                                    threadPoolFactory
                                                            .submitPeriodicTask(
                                                                    () ->
                                                                            sendRangePortPacket(toAddress, message.getMessageNumber()),
                                                                    3,
                                                                    TimeUnit.SECONDS
                                                            )
                                            );
                                            logger.info("the address of connect side is " + toAddress);
                                        }
                                    }
                                }
                            }
                        }
                )
                .putConsumer(
                        ConnectCmd.INQUIRIES,
                        (address, message) -> {
                            String to = message.getFrom();
                            if (abstractMessageMonitor.isFailMessage(message)) {
                                connectResult.put(
                                        to,
                                        Result.getFailMessage(
                                                ResponseMessageType.getResponseDescByByte(
                                                        message.getResponseMessageType()
                                                )
                                        )
                                );
                                timeRecordContainer.remove(to);
                                iThreadManager.notify(message.getMessageNumber());
                            } else {
                                Address toAddress = message.getFromAddress();
                                String identification = iGetIdentification.getIdentification();
                                if (abstractClientStarter.isRefuseConnect(toAddress, message.getSecretKey())) {
                                    sendDealTask.getTaskAndSubmit(
                                            abstractMessageWrappers
                                                    .getFailAckMessageByPacketType(
                                                            PacketType.CONNECT,
                                                            ResponseMessageType.CONNECT_REFUSED,
                                                            ConnectCmd.INQUIRIES,
                                                            identification,
                                                            "", toAddress,
                                                            message.getMessageNumber()
                                                    ),
                                            address
                                    );
                                } else {
                                    Address fromAddress = message.getToAddress();
                                    if (Objects.isNull(localPublicAddress) || !localPublicAddress.equals(fromAddress)) {
                                        localPublicAddress = fromAddress;
                                        logger.info("your local public ip is {}, port is {}",
                                                localPublicAddress.getIpv4(), localPublicAddress.getPort());
                                    }
                                }
                                if (Objects.isNull(connectedUserContainer.get(to))) {
                                    synchronized (this) {
                                        if (Objects.isNull(connectedUserContainer.get(to))) {
                                            timeRecordContainer.put(to, commonUtil.getCurTimeMillisSecond());
                                            toAddress.setStatus(ConnectStatus.CONNECTING);
                                            connectedUserContainer.put(to, toAddress);
                                            commonUtil.cancelTask(connectTaskContainer.remove(to));
                                            connectTaskContainer.put(
                                                    to,
                                                    threadPoolFactory
                                                            .submitPeriodicTask(
                                                                    () ->
                                                                            sendRangePortPacket(toAddress, message.getMessageNumber()),
                                                                    1,
                                                                    TimeUnit.SECONDS
                                                            )
                                            );
                                            logger.info("the address of connected side isZ " + toAddress);
                                        }
                                    }
                                }
                                sendDealTask.getTaskAndSubmit(
                                        abstractMessageWrappers
                                                .getAckMessageByPacketType(
                                                        PacketType.CONNECT,
                                                        ConnectCmd.INQUIRIES,
                                                        identification, to,
                                                        localPublicAddress,
                                                        toAddress,
                                                        message.getMessageNumber()
                                                ),
                                        address
                                );
                            }
                        }
                )
                .putConsumer(
                        ConnectCmd.CONNECT,
                        (address, message) -> {
                            String to = message.getFrom();
                            boolean isConnect = connectUserContainer.containsKey(to);
                            if (abstractMessageMonitor.isAck(message)) {
                                if (Objects.nonNull(timeRecordContainer.get(to))) {
                                    if (isConnect) {
                                        sendDealTask.getTaskAndSubmit(
                                                abstractMessageWrappers.getSuccessfulAckMessage(
                                                        PacketType.CONNECT,
                                                        ConnectCmd.CONNECT,
                                                        iGetIdentification.getIdentification(),
                                                        message.getMessageNumber()
                                                ),
                                                address
                                        );
                                        if (iThreadManager.notify(CommonThreadManager.CONNECT_CONDITION + message.getMessageNumber())) {
                                            connectResult.put(to, Result.getSuccessMessage("connect successfully"));
                                            commonUtil.cancelTask(connectTaskContainer.remove(to));
                                            address.setStatus(ConnectStatus.CONNECT_SUCCESS);
                                            connectUserContainer.put(to, address);
                                        }
                                    }
                                }
                            } else if (abstractMessageMonitor.isSuccessful(message)) {
                                commonUtil.cancelTask(connectTaskContainer.remove(to));
                                address.setStatus(ConnectStatus.CONNECT_SUCCESS);
                                connectedUserContainer.put(to, address);
                                logger.info("the user with the ID {}, " +
                                        "IP {}, and PORT {} connected to me", to, address.getIpv4(), address.getPort());
                                if (iThreadManager.notify(CommonThreadManager.CONNECT_CONDITION + message.getMessageNumber()))
                                    connectResult.put(to, Result.getSuccessMessage("connect successfully"));
                            } else {
                                if (abstractMessageMonitor.isFailMessage(message)) {
                                    ScheduledFuture<?> task;
                                    if (Objects.nonNull((task = connectTaskContainer.get(to)))) {
                                        if (iThreadManager.notify(CommonThreadManager.CONNECT_CONDITION + message.getMessageNumber())) {
                                            connectResult.put(to, Result.getSuccessMessage(ResponseMessageType.getResponseDescByByte(message.getResponseMessageType())));
                                            commonUtil.cancelTask(task);
                                            connectTaskContainer.remove(to);
                                            connectUserContainer.get(to).setStatus(ConnectStatus.CONNECT_SUCCESS);
                                        }
                                    }
                                } else if (iGetServerAddress.getServerAddress().equals(address)) {
                                    Address fromAddress = message.getFromAddress();
                                    if (Objects.isNull(channelTypeMap.get(fromAddress))) {
                                        synchronized (channelTypeMap) {
                                            if (Objects.isNull(channelTypeMap.get(fromAddress))) {
                                                logger.info("the connected party starts the connection");
                                                connectedUserContainer.remove(to);
                                                timeRecordContainer.remove(to);
                                                connectResult.remove(to);
                                                commonUtil.cancelTask(connectTaskContainer.remove(to));
                                                System.out.println("目标地址是" + fromAddress);
                                                channelTypeMap.put(fromAddress, ChannelType.BACKUP);
                                                clientApi.startTwoStepsConnection(to);
                                            }
                                        }
                                    }
                                } else {
                                    if (!isConnect) {
                                        sendDealTask.getTaskAndSubmit(
                                                abstractMessageWrappers.getAckMessageByPacketType(
                                                        PacketType.CONNECT,
                                                        ConnectCmd.CONNECT,
                                                        iGetIdentification.getIdentification(),
                                                        "",
                                                        null,
                                                        null,
                                                        message.getMessageNumber()
                                                ),
                                                address
                                        );
                                    }
                                }
                            }
                        }
                );
    }

    private void sendRangePortPacket(Address toAddress, String messageNumber) {
        String identification = iGetIdentification.getIdentification();
        if (localPublicAddress.getIpv4().equals(toAddress.getIpv4())) {
            sendDealTask.getTaskAndSubmit(
                    abstractMessageWrappers.getMessageByPacketType(
                            PacketType.CONNECT,
                            ConnectCmd.CONNECT,
                            identification,
                            "",
                            messageNumber,
                            null, null,
                            null,
                            null),
                    toAddress
            );
        } else {
            int count = clientInitialConfiguration.getConnectScanPortRange();
            int port = toAddress.getPort();
            int min = Math.max(0, port - count);
            int max = port + count;
            for (int j = min; j < max; j++) {
                sendDealTask.getTaskAndSubmit(
                        abstractMessageWrappers.getMessageByPacketType(
                                PacketType.CONNECT,
                                ConnectCmd.CONNECT,
                                identification,
                                "",
                                messageNumber,
                                null, null,
                                null,
                                null
                        ),
                        new Address(toAddress.getIpv4(), j)
                );
            }
        }
    }

    @Override
    public Result<Void> getConnectResult(String target) {
        return connectResult.remove(target);
    }

    @Override
    public void connectTimeoutRun(String target) {
        timeRecordContainer.remove(target);
        connectResult.remove(target);
        commonUtil.cancelTask(connectTaskContainer.remove(target));
        connectUserContainer.remove(target);
    }

    @Override
    public Address getTargetAddress(String target) {
        Address address;
        synchronized (this) {
            address = connectedUserContainer.get(target);
            if (Objects.isNull(address))
                address = connectUserContainer.get(target);
        }
        return address;
    }

    @Override
    public void connectInitialize(String identification) {
        timeRecordContainer.put(identification, commonUtil.getCurTimeMillisSecond());
    }

    @Override
    public ConnectStatus getConnectState(String target) {
        Address address;
        synchronized (this) {
            address = connectedUserContainer.get(target);
            if (Objects.isNull(address))
                address = connectUserContainer.get(target);
        }
        if (Objects.isNull(address))
            return ConnectStatus.NOT_CONNECTED;
        return address.getStatus();
    }

    @Override
    public void startTwoStepsConnect(String target, String messageNumber) {
        timeRecordContainer.put(target, commonUtil.getCurTimeMillisSecond());
        connectTaskContainer.put(target, threadPoolFactory.submitPeriodicTask(
                        () -> sendDealTask.getTaskAndSubmit(
                                abstractMessageWrappers
                                        .getMessageByPacketType(
                                                PacketType.CONNECT,
                                                ConnectCmd.CONNECT,
                                                iGetIdentification.getIdentification(),
                                                target,
                                                messageNumber,
                                                null, null, null,
                                                null
                                        ),
                                iGetServerAddress.getServerAddress()
                        ),
                        1,
                        TimeUnit.SECONDS
                )
        );
    }

    @Override
    public void addConnectTask(String target, ScheduledFuture<?> connectTaskFuture) {
        connectTaskContainer.put(target, connectTaskFuture);
    }

    @Override
    public ChannelType obtainChannelType(Address address) {
        if (Objects.isNull(address))
            return ChannelType.DEFAULT;
        return channelTypeMap.get(address);
    }
}
